Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / core / data / src / commonTest / kotlin / org / meshtastic / core / data / manager / MeshConnectionManagerImplTest.kt

Displaying Raw β€’ Download

core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt 3ca87fa0323214c7204c9d37acfe9d4b6ab6ea8b (3ca87fa0) Text, 54.13 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.data.manager

Tff7b72import T7ee787co.touchlab.kermit.Severity
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.calls
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.everySuspend
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787dev.mokkery.verify
Tff7b72import T7ee787dev.mokkery.verify.VerifyMode.Companion.exactly
Tff7b72import T7ee787dev.mokkery.verifySuspend
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.flowOf
Tff7b72import T7ee787kotlinx.coroutines.test.StandardTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.TestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runCurrent
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.Node
Tff7b72import T7ee787org.meshtastic.core.repository.AppWidgetUpdater
Tff7b72import T7ee787org.meshtastic.core.repository.CommandSender
Tff7b72import T7ee787org.meshtastic.core.repository.HistoryManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshLocationManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshNotificationManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshWorkerManager
Tff7b72import T7ee787org.meshtastic.core.repository.MqttManager
Tff7b72import T7ee787org.meshtastic.core.repository.NodeManager
Tff7b72import T7ee787org.meshtastic.core.repository.PacketHandler
Tff7b72import T7ee787org.meshtastic.core.repository.PacketRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PlatformAnalytics
Tff7b72import T7ee787org.meshtastic.core.repository.RadioConfigRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceRepository
Tff7b72import T7ee787org.meshtastic.core.repository.SessionManager
Tff7b72import T7ee787org.meshtastic.core.repository.UiPrefs
Tff7b72import T7ee787org.meshtastic.core.testing.FakeNodeRepository
Tff7b72import T7ee787org.meshtastic.proto.Config
Tff7b72import T7ee787org.meshtastic.proto.LocalConfig
Tff7b72import T7ee787org.meshtastic.proto.LocalModuleConfig
Tff7b72import T7ee787org.meshtastic.proto.ModuleConfig
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertTrue

Tf0883e@OptInTb4b4b4(Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364MeshConnectionManagerImplTest Tb4b4b4{
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceService
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3serviceRepositoryTb4b4b4: Te6edf3ServiceRepository

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3serviceNotificationsTb4b4b4: Te6edf3MeshNotificationManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3uiPrefsTb4b4b4: Te6edf3UiPrefs
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3packetHandlerTb4b4b4: Te6edf3PacketHandler
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3nodeRepositoryTb4b4b4: Te6edf3FakeNodeRepository
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3locationManagerTb4b4b4: Te6edf3MeshLocationManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3mqttManagerTb4b4b4: Te6edf3MqttManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3historyManagerTb4b4b4: Te6edf3HistoryManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3radioConfigRepositoryTb4b4b4: Te6edf3RadioConfigRepository
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3commandSenderTb4b4b4: Te6edf3CommandSender
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3sessionManagerTb4b4b4: Te6edf3SessionManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3nodeManagerTb4b4b4: Te6edf3NodeManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3analyticsTb4b4b4: Te6edf3PlatformAnalytics
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3packetRepositoryTb4b4b4: Te6edf3PacketRepository
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3workerManagerTb4b4b4: Te6edf3MeshWorkerManager
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3appWidgetUpdaterTb4b4b4: Te6edf3AppWidgetUpdater

Tff7b72private Tff7b72val Te6edf3dataPacket Tff7b72= Te6edf3DataPacketTb4b4b4(Te6edf3id Tff7b72= T79c0ff4T79c0ff5T79c0ff6Tb4b4b4, Te6edf3time Tff7b72= T79c0ff0LTb4b4b4, Te6edf3to Tff7b72= Ta5d6ff"Ta5d6ff0Ta5d6ff"Tb4b4b4, Te6edf3from Tff7b72= Ta5d6ff"Ta5d6ff0Ta5d6ff"Tb4b4b4, Te6edf3bytes Tff7b72= Tff7b72nullTb4b4b4, Te6edf3dataType Tff7b72= T79c0ff0Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3radioConnectionState Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Tff7b72private Tff7b72val Te6edf3connectionStateFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Tff7b72private Tff7b72val Te6edf3localConfigFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Te6edf3LocalConfigTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3moduleConfigFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Te6edf3LocalModuleConfigTb4b4b4(Tb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3testDispatcherTb4b4b4: Te6edf3TestDispatcher

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3managerTb4b4b4: Te6edf3MeshConnectionManagerImpl

Tf0883e@BeforeTest
Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioInterfaceService Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3serviceRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3serviceNotifications Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3uiPrefs Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3packetHandler Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3nodeRepository Tff7b72= Te6edf3FakeNodeRepositoryTb4b4b4(Tb4b4b4)
Te6edf3locationManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3mqttManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3historyManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3radioConfigRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3commandSender Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3sessionManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3nodeManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3analytics Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3packetRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3workerManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Te6edf3appWidgetUpdater Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3localConfigFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3LocalConfigTb4b4b4(Tb4b4b4)
Te6edf3moduleConfigFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3LocalModuleConfigTb4b4b4(Tb4b4b4)

Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectionState Tb4b4b4} Te6edf3returns Te6edf3radioConnectionState
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3localConfigFlow
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3moduleConfigFlow Tb4b4b4} Te6edf3returns Te6edf3moduleConfigFlow
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionState Tb4b4b4} Te6edf3returns Te6edf3connectionStateFlow
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3serviceNotificationsTb4b4b4.Te6edf3updateServiceStateNotificationTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3everySuspend Tb4b4b4{ Te6edf3commandSenderTb4b4b4.Te6edf3sendAdminTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3stopPacketQueueTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3locationManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3mqttManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTff7b72<Tffa657IntTb4b4b4, Te6edf3NodeTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3anyTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffcreateManagerTb4b4b4(Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4)Tb4b4b4: Te6edf3MeshConnectionManagerImpl Tff7b72= Te6edf3MeshConnectionManagerImplTb4b4b4(
Te6edf3radioInterfaceServiceTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4,
Te6edf3serviceNotificationsTb4b4b4,
Te6edf3uiPrefsTb4b4b4,
Te6edf3packetHandlerTb4b4b4,
Te6edf3nodeRepositoryTb4b4b4,
Te6edf3locationManagerTb4b4b4,
Te6edf3mqttManagerTb4b4b4,
Te6edf3historyManagerTb4b4b4,
Te6edf3radioConfigRepositoryTb4b4b4,
Te6edf3commandSenderTb4b4b4,
Te6edf3sessionManagerTb4b4b4,
Te6edf3nodeManagerTb4b4b4,
Te6edf3analyticsTb4b4b4,
Te6edf3packetRepositoryTb4b4b4,
Te6edf3workerManagerTb4b4b4,
Te6edf3appWidgetUpdaterTb4b4b4,
Te6edf3DataLayerHeartbeatSenderTb4b4b4(Te6edf3packetHandlerTb4b4b4)Tb4b4b4,
Te6edf3scopeTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffrestartTransportCallCounterTb4b4b4(Tb4b4b4)Tb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Int Tb4b4b4{
Tff7b72var Te6edf3restartCalls Tff7b72= T79c0ff0
Te6edf3everySuspend Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls Tb4b4b4{ Te6edf3restartCalls Tff7b72+Tff7b72= T79c0ff1 Tb4b4b4}
Tff7b72return Tb4b4b4{ Te6edf3restartCalls Tb4b4b4}
Tb4b4b4}

Tf0883e@AfterTest Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tff7b72= Tffa657Unit

Tf0883e@Test
Tff7b72fun Td2a8ff`Connected state triggers broadcast and config start`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffState should be Connecting after radio ConnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Connected state sends pre-handshake heartbeat before config request`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3sentPackets Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3anyTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3sentPacketsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3callTb4b4b4.Te6edf3argTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Advance past PRE_HANDSHAKE_SETTLE_MS (100ms) but NOT the 30s stall guard timeout
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)

T8b949e// First ToRadio should be a heartbeat, second should be want_config_id
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3sentPacketsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffExpected heartbeat + want_config_id, got Tffd700${Te6edf3sentPacketsTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff packetsTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3heartbeat Tff7b72= Te6edf3sentPacketsTff7b72[T79c0ff0Tff7b72]
Tff7b72val Te6edf3wantConfig Tff7b72= Te6edf3sentPacketsTff7b72[T79c0ff1Tff7b72]

Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3heartbeatTb4b4b4.Te6edf3heartbeat Tff7b72!Tff7b72= Tff7b72nullTb4b4b4, Ta5d6ff"Ta5d6ffFirst packet should be a heartbeatTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Tff7b72trueTb4b4b4, Te6edf3heartbeatTb4b4b4.Te6edf3heartbeatTff7b72!!Tb4b4b4.Te6edf3nonce Tff7b72!Tff7b72= T79c0ff0Tb4b4b4, Ta5d6ff"Ta5d6ffHeartbeat should have a non-zero nonceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3repositoryTb4b4b4.Te6edf3HandshakeConstantsTb4b4b4.Te6edf3CONFIG_NONCETb4b4b4,
Te6edf3wantConfigTb4b4b4.Te6edf3want_config_idTb4b4b4,
Ta5d6ff"Ta5d6ffSecond packet should be want_config_id with CONFIG_NONCETa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Disconnect during pre-handshake settle cancels config start`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3sentPackets Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3anyTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3sentPacketsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3callTb4b4b4.Te6edf3argTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Advance only 50ms β€” within the 100ms settle window
Te6edf3advanceTimeByTb4b4b4(T79c0ff5T79c0ff0Tb4b4b4)

T8b949e// Should have sent only the heartbeat so far, not want_config_id
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3sentPacketsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffOnly heartbeat should be sent before settle completesTa5d6ff"Tb4b4b4)

T8b949e// Disconnect before the settle delay completes β€” should cancel the pending config start
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)

T8b949e// The want_config_id should NOT have been sent because the job was cancelled
Tff7b72val Te6edf3configPackets Tff7b72= Te6edf3sentPacketsTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3want_config_id Tff7b72!Tff7b72= Tff7b72null Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3configPacketsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffwant_config_id should not be sent after disconnectTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Disconnected state stops services`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
T8b949e// Transition to Connected first so that Disconnected actually does something
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffState should be Disconnected after radio DisconnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3verify Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3stopPacketQueueTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3locationManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3mqttManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`DeviceSleep behavior when power saving is off maps to Disconnected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Power saving disabled + Role CLIENT
Tff7b72val Te6edf3config Tff7b72=
Te6edf3LocalConfigTb4b4b4(
Te6edf3power Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3PowerConfigTb4b4b4(Te6edf3is_power_saving Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3device Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3DeviceConfigTb4b4b4(Te6edf3role Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3DeviceConfigTb4b4b4.Te6edf3RoleTb4b4b4.Te6edf3CLIENTTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3configTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffState should be Disconnected when power saving is offTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`DeviceSleep behavior when power saving is on stays in DeviceSleep`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Power saving enabled
Tff7b72val Te6edf3config Tff7b72= Te6edf3LocalConfigTb4b4b4(Te6edf3power Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3PowerConfigTb4b4b4(Te6edf3is_power_saving Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3configTb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleepTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffState should stay in DeviceSleep when power saving is onTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onRadioConfigLoaded enqueues queued packets and sets time`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Tff7b72val Te6edf3packetId Tff7b72= T79c0ff4T79c0ff5T79c0ff6
Te6edf3everySuspend Tb4b4b4{ Te6edf3packetRepositoryTb4b4b4.Te6edf3getQueuedPacketsTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3listOfTb4b4b4(Te6edf3dataPacketTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3workerManagerTb4b4b4.Te6edf3enqueueSendMessageTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit

Te6edf3managerTb4b4b4.Te6edf3onRadioConfigLoadedTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3workerManagerTb4b4b4.Te6edf3enqueueSendMessageTb4b4b4(Te6edf3packetIdTb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onNodeDbReady starts MQTT and requests history`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3moduleConfig Tff7b72=
Te6edf3LocalModuleConfigTb4b4b4(
Te6edf3mqtt Tff7b72= Te6edf3ModuleConfigTb4b4b4.Te6edf3MQTTConfigTb4b4b4(Te6edf3enabled Tff7b72= Tff7b72trueTb4b4b4, Te6edf3proxy_to_client_enabled Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4,
Te6edf3store_forward Tff7b72= Te6edf3ModuleConfigTb4b4b4.Te6edf3StoreForwardConfigTb4b4b4(Te6edf3enabled Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3moduleConfigFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3moduleConfig
Te6edf3everySuspend Tb4b4b4{ Te6edf3commandSenderTb4b4b4.Te6edf3requestTelemetryTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(T79c0ff1T79c0ff2T79c0ff3Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3mqttManagerTb4b4b4.Te6edf3startProxyTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3everySuspend Tb4b4b4{ Te6edf3historyManagerTb4b4b4.Te6edf3requestHistoryReplayTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3getMyNodeInfoTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tff7b72null

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3onNodeDbReadyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3mqttManagerTb4b4b4.Te6edf3startProxyTb4b4b4(Tff7b72trueTb4b4b4, Tff7b72trueTb4b4b4) Tb4b4b4}
Te6edf3verifySuspend Tb4b4b4{ Te6edf3historyManagerTb4b4b4.Te6edf3requestHistoryReplayTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`DeviceSleep timeout is capped at MAX_SLEEP_TIMEOUT_SECONDS for high ls_secs`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Router with ls_secs=3600 β€” previously this created a 3630s timeout.
T8b949e// With the cap, it should be clamped to 300s.
Tff7b72val Te6edf3config Tff7b72=
Te6edf3LocalConfigTb4b4b4(
Te6edf3power Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3PowerConfigTb4b4b4(Te6edf3is_power_saving Tff7b72= Tff7b72trueTb4b4b4, Te6edf3ls_secs Tff7b72= T79c0ff3T79c0ff6T79c0ff0T79c0ff0Tb4b4b4)Tb4b4b4,
Te6edf3device Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3DeviceConfigTb4b4b4(Te6edf3role Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3DeviceConfigTb4b4b4.Te6edf3RoleTb4b4b4.Te6edf3ROUTERTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3configTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Transition to Connected then DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleepTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffShould be in DeviceSleep initiallyTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Advance 300 seconds (the cap) + 1 second to trigger the timeout.
Te6edf3advanceTimeByTb4b4b4(T79c0ff3T79c0ff0T79c0ff1Te6edf3_000LTb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffShould transition to Disconnected after capped timeout (300s), not the raw 3630sTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`rapid state transitions are serialized by connectionMutex`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Power saving enabled so DeviceSleep is preserved (not mapped to Disconnected)
Tff7b72val Te6edf3config Tff7b72= Te6edf3LocalConfigTb4b4b4(Te6edf3power Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3PowerConfigTb4b4b4(Te6edf3is_power_saving Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3configTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)

T8b949e// Record every state transition so we can verify ordering
Tff7b72val Te6edf3observed Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Tff7b72val Te6edf3state Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3observedTb4b4b4.Te6edf3addTb4b4b4(Te6edf3stateTb4b4b4)
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3state
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Rapid-fire: Connected -> DeviceSleep -> Disconnected without yielding between them.
T8b949e// Without the Mutex, the intermediate DeviceSleep could be missed or applied out of order.
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Verify final state
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffFinal state should be Disconnected after rapid transitionsTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Verify that all intermediate states were observed in correct order.
T8b949e// Connected triggers handleConnected() which sets Connecting (handshake start),
T8b949e// then DeviceSleep, then Disconnected.
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4, Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleepTb4b4b4, Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)Tb4b4b4,
Te6edf3observedTb4b4b4,
Ta5d6ff"Ta5d6ffState transitions should be serialized in order: Connecting -> DeviceSleep -> DisconnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`concurrent sleep-timeout and radio state change are serialized`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3standardDispatcher Tff7b72= Te6edf3StandardTestDispatcherTb4b4b4(Tb4b4b4)
Te6edf3runTestTb4b4b4(Te6edf3standardDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Power saving enabled with ls_secs=0 so the sleep timeout boundary is just before the
T8b949e// Stage 1 handshake watchdog. That keeps this test isolated to sleep-timeout behavior.
Tff7b72val Te6edf3config Tff7b72= Te6edf3LocalConfigTb4b4b4(Te6edf3power Tff7b72= Te6edf3ConfigTb4b4b4.Te6edf3PowerConfigTb4b4b4(Te6edf3is_power_saving Tff7b72= Tff7b72trueTb4b4b4, Te6edf3ls_secs Tff7b72= T79c0ff0Tb4b4b4)Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3localConfigFlow Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3configTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNum Tb4b4b4} Te6edf3returns Te6edf3emptyMapTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3observed Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Tff7b72val Te6edf3state Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3observedTb4b4b4.Te6edf3addTb4b4b4(Te6edf3stateTb4b4b4)
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3state
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

T8b949e// Transition to Connected -> DeviceSleep to start the sleep timer
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

Te6edf3observedTb4b4b4.Te6edf3clearTb4b4b4(Tb4b4b4)

T8b949e// Before the sleep timeout fires, emit Connected from the radio (simulating device
T8b949e// waking up). Then let the timeout fire. The mutex ensures they don't race.
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3runCurrentTb4b4b4(Tb4b4b4)
T8b949e// Advance past the sleep timeout (ls_secs=0 + 30s base), but stop before the Stage 1
T8b949e// handshake watchdog fires at roughly 30.1s after the wake-up Connected signal.
Te6edf3advanceTimeByTb4b4b4(T79c0ff3T79c0ff0Te6edf3_050LTb4b4b4)
Te6edf3runCurrentTb4b4b4(Tb4b4b4)

T8b949e// The Connected transition should have cancelled the sleep timeout, so we should
T8b949e// end up in Connecting (from handleConnected), NOT Disconnected (from timeout).
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffConnected should cancel the sleep timeout; final state should be ConnectingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Stage 1 config stall triggers transport restart and ends Disconnected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
T8b949e// Disconnected -> Connected: handleConnected() sets Connecting, sends pre-handshake
T8b949e// heartbeat, and (after PRE_HANDSHAKE_SETTLE_MS=100ms) calls startConfigOnly() which
T8b949e// arms the Stage 1 stall guard (HANDSHAKE_TIMEOUT_STAGE1 = 30s).
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Pre-condition: Stage 1 is in flight β€” manager is Connecting and a ToRadio has been sent
T8b949e// (heartbeat + want_config_id). Use at-least-one here so the test isn't brittle on the
T8b949e// exact packet count.
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should be Connecting after radio ConnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3verify Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3anyTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}

T8b949e// Advance past HANDSHAKE_TIMEOUT_STAGE1 (30s) WITHOUT any config arrival. The stall
T8b949e// fires the recovery sibling unconditionally on every transport β€” there is no longer a
T8b949e// want_config retry delay before restart. The production code runs BOTH transitions
T8b949e// inside one sibling recovery job β€” onConnectionChanged(Disconnected) FIRST, then
T8b949e// restartTransport() β€” so the fresh Connected emission from restartTransport arrives
T8b949e// with app-level state already Disconnected and is not ignored by the redundant-
T8b949e// Connecting guard in onConnectionChanged. The advanceTimeBy keeps extra slack so the
T8b949e// test stays robust against small timeout tweaks.
Te6edf3advanceTimeByTb4b4b4(T79c0ff4T79c0ff6Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffStage 1 stall should end in Disconnected after restart is requestedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Handshake stall recovery orders app disconnect before transport restart emissions`Tb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// This test locks in the ordering invariant of the stall recovery
T8b949e// sibling: onConnectionChanged(Disconnected) runs FIRST, then restartTransport().
T8b949e// We deliberately do NOT stub restartTransport() β€” the default mock no-op leaves it
T8b949e// as a pure boundary call so the sibling's two phases can be observed independently.
T8b949e//
T8b949e// After the stall fires and the sibling completes, we MANUALLY replay the
T8b949e// transport-level emissions that the real restartTransport() would produce:
T8b949e// - DeviceSleep (onDisconnect(isPermanent=false) on the old transport)
T8b949e// - Connected (the new transport's onConnect callback)
T8b949e// Under the FIXED ordering, the fresh Connected arrives with app state already
T8b949e// Disconnected, bypasses the redundant-Connecting guard in onConnectionChanged,
T8b949e// and re-enters handleConnected β†’ state returns to Connecting.
T8b949e// Under the BROKEN (old) ordering β€” restartTransport() BEFORE Disconnected β€” the
T8b949e// fresh Connected would arrive while app state was still Connecting, the redundant-
T8b949e// Connecting guard would drop it, and the state would never return to Connecting.
T8b949e//
T8b949e// Restructured to be deterministic on JVM CI: rather than relying on a stubbed
T8b949e// restartTransport() lambda whose StateFlow side-effect emissions race with the
T8b949e// flow collector under Mokkery, the test body itself drives the emissions in order.
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
T8b949e// Disconnected -> Connected: handleConnected() sets Connecting, sends pre-handshake
T8b949e// heartbeat, and (after PRE_HANDSHAKE_SETTLE_MS=100ms) calls startConfigOnly() which
T8b949e// arms the Stage 1 stall guard (HANDSHAKE_TIMEOUT_STAGE1 = 30s).
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Pre-condition: Stage 1 is in flight.
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should be Connecting after radio ConnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Advance past HANDSHAKE_TIMEOUT_STAGE1 (30s) WITHOUT any config arrival. The stall
T8b949e// fires the recovery sibling: onConnectionChanged(Disconnected) FIRST, then
T8b949e// restartTransport() (default mock no-op, so nothing re-arms a stall guard and
T8b949e// advanceUntilIdle is safe here).
Te6edf3advanceTimeByTb4b4b4(T79c0ff4T79c0ff6Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffSibling must run onConnectionChanged(Disconnected) BEFORE restartTransport() β€” Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffproves the app-level Disconnected transition landedTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Manually replay the transport-level restart signals that the real restartTransport()
T8b949e// would emit. DeviceSleep corresponds to onDisconnect(isPermanent=false) on the old
T8b949e// transport; Connected corresponds to the new transport's onConnect callback. With
T8b949e// UnconfinedTestDispatcher each emission is collected synchronously inline, so no
T8b949e// advanceUntilIdle() is needed between them β€” and none is safe AFTER the Connected
T8b949e// emission, because handleConnected re-arms a fresh Stage 1 stall guard and
T8b949e// advanceUntilIdle would advance virtual time past it (and every subsequent re-arm),
T8b949e// looping the recovery and obscuring the single-shot ordering this test locks in.
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffFresh Connected emission must re-enter handleConnected (NOT be ignored by the Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffredundant-Connecting guard) because the app-level Disconnected transition Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffalready ran BEFORE restartTransport's transport cycle β€” this is the ordering Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffinvariant under testTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Stage 2 node-info stall triggers transport restart`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Pre-handshake settle completes; Stage 1 stall guard armed.
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Drive the connection into Stage 2. In production this is done by the config-flow
T8b949e// manager once Stage 1 config arrives; here we invoke it directly. startNodeInfoOnly()
T8b949e// cancels the Stage 1 stall guard and arms Stage 2 (HANDSHAKE_TIMEOUT_STAGE2 = 60s).
Te6edf3managerTb4b4b4.Te6edf3startNodeInfoOnlyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should still be Connecting entering Stage 2Ta5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Advance past HANDSHAKE_TIMEOUT_STAGE2 (60s) WITHOUT invoking onNodeDbReady(). The
T8b949e// stall must fire the recovery sibling unconditionally.
Te6edf3advanceTimeByTb4b4b4(T79c0ff7T79c0ff6Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffStage 2 stall should end in Disconnected after restart is requestedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Handshake completing before stall timeout does not trigger transport restart`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Stubs required by onNodeDbReady() (full handshake completion path).
Te6edf3everySuspend Tb4b4b4{ Te6edf3commandSenderTb4b4b4.Te6edf3requestTelemetryTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(T79c0ff1T79c0ff2T79c0ff3Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3mqttManagerTb4b4b4.Te6edf3startProxyTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3everySuspend Tb4b4b4{ Te6edf3historyManagerTb4b4b4.Te6edf3requestHistoryReplayTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3returns Tffa657Unit
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3getMyNodeInfoTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Tff7b72null

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Pre-handshake settle completes; Stage 1 stall guard armed.
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Simulate the full handshake completing (config arrives + NodeDB becomes ready).
T8b949e// onNodeDbReady() cancels handshakeTimeout, so the stall recovery sibling can
T8b949e// never run even if virtual time later crosses the stage windows.
Te6edf3managerTb4b4b4.Te6edf3onNodeDbReadyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Advance well past BOTH stage windows (Stage 1: 30s, Stage 2: 60s).
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff2T79c0ff0Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP Stage 1 stall fires restartTransport at 12s without retry`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Address starting with 't' β†’ DeviceType.TCP β†’ fast recovery transport.
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Tff7b72val Te6edf3sentPackets Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3packetHandlerTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3anyTff7b72<Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3protoTb4b4b4.Te6edf3ToRadioTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3sentPacketsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3callTb4b4b4.Te6edf3argTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Pre-handshake settle (100ms): heartbeat + Stage 1 want_config_id sent, fast watchdog armed.
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should be Connecting after radio ConnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Sanity: heartbeat + Stage 1 want_config_id have been sent.
Tff7b72val Te6edf3packetsBeforeStall Tff7b72= Te6edf3sentPacketsTb4b4b4.Te6edf3size

T8b949e// Advance past FAST_HANDSHAKE_TIMEOUT (12s) WITHOUT any progress signal. The fast
T8b949e// branch must fire the recovery sibling directly β€” no same-session want_config re-send.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffFast stall should end in Disconnected after restart is requestedTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// The recovery sibling must not re-send want_config_id on the same session: no additional
T8b949e// packet may be sent between the initial arming and the restartTransport call (a same-
T8b949e// session re-send re-enters firmware handleStartConfig() and crashes the device).
Te6edf3assertEqualsTb4b4b4(
Te6edf3packetsBeforeStallTb4b4b4,
Te6edf3sentPacketsTb4b4b4.Te6edf3sizeTb4b4b4,
Ta5d6ff"Ta5d6ffFast transport stall must NOT trigger a retry send of want_config_idTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP Stage 1 meaningful progress resets watchdog without false restart`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Trickle inbound progress signals β€” each lands well inside the 12s fast window, so
T8b949e// each one re-arms the watchdog and no restart may fire across multiple resets.
Te6edf3repeatTb4b4b4(T79c0ff5Tb4b4b4) Tb4b4b4{
Te6edf3advanceTimeByTb4b4b4(T79c0ff8Te6edf3_000LTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffSteady trickle of progress must keep the manager ConnectingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP Stage 1 watchdog fires after progress stops`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Two progress signals land inside the window.
Te6edf3advanceTimeByTb4b4b4(T79c0ff8Te6edf3_000LTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff8Te6edf3_000LTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Progress stops; advance past the fast timeout from the last re-arm.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffWatchdog must fire once progress stops for longer than the fast timeoutTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP Stage 2 stall fires restartTransport at 12s`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Drive the connection into Stage 2. In production this is done by the config-flow
T8b949e// manager once Stage 1 config arrives; here we invoke it directly. startNodeInfoOnly()
T8b949e// cancels the Stage 1 watchdog and arms Stage 2 with the fast timeout (12s on TCP).
Te6edf3managerTb4b4b4.Te6edf3startNodeInfoOnlyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should still be Connecting entering Stage 2Ta5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Advance past the fast timeout WITHOUT invoking onNodeDbReady().
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffStage 2 fast stall should end in Disconnected after restart is requestedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP Stage 2 NodeInfo progress resets watchdog`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3startNodeInfoOnlyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stage 2 node-info burst packets arrive as a trickle; each resets the fast watchdog.
Te6edf3repeatTb4b4b4(T79c0ff3Tb4b4b4) Tb4b4b4{
Te6edf3advanceTimeByTb4b4b4(T79c0ff7Te6edf3_000LTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffStage 2 progress must keep the manager ConnectingTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`TCP fast recovery preserves Disconnected-before-restartTransport ordering`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"

Tff7b72val Te6edf3observed Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3progressBeforeRestartTb4b4b4: Tffa657String? Tff7b72= Tff7b72null
Tff7b72var Te6edf3restartTransportCalls Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Tff7b72val Te6edf3state Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3observedTb4b4b4.Te6edf3addTb4b4b4(Te6edf3stateTb4b4b4)
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3state
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionProgressTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
T8b949e// Capture the most recent progress string at the moment restartTransport runs.
T8b949e// This locks in that the "Reconnecting…" UX hook fires BEFORE the app-level
T8b949e// Disconnected transition.
Te6edf3progressBeforeRestart Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Tffa657StringTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Tb4b4b4}
Te6edf3everySuspend Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3restartTransportCallsTff7b72+Tff7b72+
T8b949e// At the moment restartTransport runs, the app-level state MUST already be
T8b949e// Disconnected β€” otherwise the fresh transport Connected emission would be
T8b949e// dropped by the redundant-Connecting guard and we would re-introduce the
T8b949e// split-brain this recovery path exists to break.
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3connectionStateFlowTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffrestartTransport must run AFTER app-level Disconnected transitionTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3ServiceRepositoryTb4b4b4.Te6edf3RECONNECTING_PROGRESS_TEXTTb4b4b4,
Te6edf3progressBeforeRestartTb4b4b4,
Ta5d6ff"Ta5d6ffsetConnectionProgress(ServiceRepository.RECONNECTING_PROGRESS_TEXT) must run before restartTransportTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Fire fast stall.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4)
T8b949e// Sanity: Connecting β†’ Disconnected was the observed app-level transition path.
Te6edf3assertEqualsTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4, Te6edf3observedTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffLast app-level state must be DisconnectedTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`post-handshake failure recovery disconnects before restarting transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"

Tff7b72val Te6edf3observed Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3progressBeforeRestartTb4b4b4: Tffa657String? Tff7b72= Tff7b72null
Tff7b72var Te6edf3restartTransportCalls Tff7b72= T79c0ff0
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionStateTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Tff7b72val Te6edf3state Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3observedTb4b4b4.Te6edf3addTb4b4b4(Te6edf3stateTb4b4b4)
Te6edf3connectionStateFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3state
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setConnectionProgressTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3progressBeforeRestart Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Tffa657StringTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4)
Tb4b4b4}
Te6edf3everySuspend Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3restartTransportCallsTff7b72+Tff7b72+
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3connectionStateFlowTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffpost-handshake recovery must disconnect app state before restarting transportTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3ServiceRepositoryTb4b4b4.Te6edf3RECONNECTING_PROGRESS_TEXTTb4b4b4,
Te6edf3progressBeforeRestartTb4b4b4,
Ta5d6ff"Ta5d6ffpost-handshake recovery should surface reconnecting progress before restartTransportTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3onHandshakeCompleteTb4b4b4(Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3recoverPostHandshakeFailureTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4, Te6edf3observedTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffLast app-level state must be DisconnectedTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`BLE transport unaffected by onHandshakeProgress regression`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Explicit BLE address (starts with 'x') β€” must NOT engage the fast path.
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6ffxAA:BB:CC:DD:EE:FFTa5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Spam progress signals β€” BLE must ignore them, so the 30s Stage 1 budget is unchanged.
Te6edf3repeatTb4b4b4(T79c0ff1T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e// Advance to 13s β€” would fire a fast watchdog if BLE were incorrectly included.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffBLE must ignore onHandshakeProgress and stay Connecting through 13sTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Now advance past the full BLE Stage 1 budget (30s). We already advanced 13s, so 33s
T8b949e// more crosses the 30s Stage 1 stall threshold the recovery sibling fires at. (Extra
T8b949e// slack is harmless β€” the BLE stage has no retry delay before recovery.)
Te6edf3advanceTimeByTb4b4b4(T79c0ff3T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onHandshakeProgress is a no-op when state is not Connecting`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
T8b949e// Manager starts in Disconnected (initial state, no radio Connected signal yet).
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffPrecondition: manager must start DisconnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Calling onHandshakeProgress while not Connecting must not arm any watchdog.
Te6edf3managerTb4b4b4.Te6edf3onHandshakeProgressTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff6T79c0ff0Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`USB serial transport engages fast path like TCP`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Address starting with 's' β†’ DeviceType.USB β†’ fast recovery transport.
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6ffs/dev/ttyUSB0Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Fast timeout (12s) fires for USB just like TCP.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffUSB fast stall should end in Disconnected after restart is requestedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`onHandshakeComplete cancels armed Stage 2 fast watchdog`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// TCP address β†’ DeviceType.TCP β†’ fast recovery transport, 12s fast watchdog.
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
T8b949e// Pre-handshake settle (100ms) completes; Stage 1 fast watchdog armed.
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffManager should be Connecting after radio ConnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Drive into Stage 2. startNodeInfoOnly() cancels the Stage 1 watchdog and arms Stage 2
T8b949e// with the 12s fast timeout. In production this is invoked by MeshConfigFlowManagerImpl
T8b949e// after Stage 1 completes; here we drive it directly to isolate the watchdog path.
Te6edf3managerTb4b4b4.Te6edf3startNodeInfoOnlyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Synchronously cancel the watchdog, exactly as MeshConfigFlowManagerImpl now does the
T8b949e// instant NODE_INFO_NONCE arrives β€” BEFORE the async DB install work begins.
Te6edf3managerTb4b4b4.Te6edf3onHandshakeCompleteTb4b4b4(Tb4b4b4)

T8b949e// Advance past the 12s fast timeout. With the watchdog cancelled, the recovery
T8b949e// sibling must NOT fire: no restartTransport, no state transition.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3restartTransportTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectingTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffWatchdog was cancelled by onHandshakeComplete β€” no restart, no state transitionTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Second recovery attempt applies exponential backoff delay before restart`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// TCP address β†’ fast path (12s stall window). Smallest reliable cycle for backoff testing.
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Tff7b72val Te6edf3restartTransportCalls Tff7b72= Te6edf3restartTransportCallCounterTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
T8b949e// Initial Connected β†’ Connecting, pre-handshake armed.
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #1 fires (priorFailures=0 β†’ no backoff). restartTransport called exactly once.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)

T8b949e// Replay the transport restart cycle so handleConnected re-arms the stall guard. DeviceSleep
T8b949e// is a no-op here (power-saving off); Connected re-enters handleConnected β†’ new pre-handshake.
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #2 fires (priorFailures=1 β†’ 2 s backoff before restartTransport). The sibling has
T8b949e// launched but is suspended in delay(2.seconds). advanceUntilIdle cannot make progress
T8b949e// because the only pending task is the timed delay.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
T8b949e// Prove the backoff is in effect: restartTransport must NOT have been called yet for the
T8b949e// second stall. If the backoff were skipped, this would already be exactly(2).
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)

T8b949e// Advancing past the 2 s backoff resumes the sibling and triggers the second restart.
Te6edf3advanceTimeByTb4b4b4(T79c0ff2Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Recovery exhaustion after three consecutive failures surfaces sticky error and stops recovery`Tb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"

T8b949e// Capture every setErrorMessage invocation so we can verify the sticky error surfaced
T8b949e// with Severity.Error without depending on the exact localized text.
Tff7b72val Te6edf3errorMessages Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3PairTff7b72<Tffa657StringTb4b4b4, Te6edf3SeverityTff7b72>Tff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{ Te6edf3call Tff7b72-Tff7b72>
Te6edf3errorMessages Tff7b72+Tff7b72= Te6edf3callTb4b4b4.Te6edf3argTff7b72<Tffa657StringTff7b72>Tb4b4b4(T79c0ff0Tb4b4b4) Te6edf3to Te6edf3callTb4b4b4.Te6edf3argTff7b72<Te6edf3SeverityTff7b72>Tb4b4b4(T79c0ff1Tb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3restartTransportCalls Tff7b72= Te6edf3restartTransportCallCounterTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #1 (priorFailures=0): no delay β†’ restart. Replay Connected.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #2 (priorFailures=1): 2 s backoff β†’ restart. Replay Connected.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff2Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #3 (priorFailures=2): 4 s backoff β†’ restart. Replay Connected.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff4Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #4 (priorFailures=3 = MAX_CONSECUTIVE_RECOVERY_FAILURES): sticky error path.
T8b949e// The sibling must NOT call restartTransport; instead it resets the counter, clears the
T8b949e// progress text, transitions to Disconnected, and surfaces the sticky error.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4,
Te6edf3serviceRepositoryTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffRecovery exhaustion must leave the manager in DisconnectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// The sticky error is surfaced via setConnectionProgress("") (cleared because the user
T8b949e// must manually retry) plus setErrorMessage(..., Severity.Error). We assert at least one
T8b949e// setErrorMessage call landed with Severity.Error β€” this is the user-visible signal that
T8b949e// recovery gave up. (Exact text comes from Res.string.error_recovery_exhausted and is
T8b949e// resolved by the production code via getStringSuspend.)
Te6edf3assertTrueTb4b4b4(
Te6edf3errorMessagesTb4b4b4.Te6edf3any Tb4b4b4{ Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3severityTb4b4b4) Tff7b72-Tff7b72> Te6edf3severity Tff7b72=Tff7b72= Te6edf3SeverityTb4b4b4.Te6edf3Error Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffRecovery exhaustion must surface a sticky ERROR-severity message; got: Tffd700$Te6edf3errorMessagesTa5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// Counter is reset to 0 by the sticky-error branch so a manual user retry starts fresh.
T8b949e// We can't read the private atomic directly, but the reset is exercised by the next-stall
T8b949e// behavior; its correctness is locked in by `onHandshakeComplete resets counter` and by
T8b949e// the symmetric reset-on-exhaust code path in runSiblingHandshakeRecovery().
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`Successful handshake resets the consecutive recovery failure counter`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Ta5d6ff"Ta5d6fft192.168.1.42Ta5d6ff"
Tff7b72val Te6edf3restartTransportCalls Tff7b72= Te6edf3restartTransportCallCounterTb4b4b4(Tb4b4b4)

Te6edf3manager Tff7b72= Te6edf3createManagerTb4b4b4(Te6edf3backgroundScopeTb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #1 (priorFailures=0): no delay β†’ restart.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #2 (priorFailures=1): 2 s backoff β†’ restart. Counter now at 2.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3advanceTimeByTb4b4b4(T79c0ff2Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Te6edf3radioConnectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3advanceTimeByTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// The next stall WOULD see priorFailures=2 (4 s backoff) if the counter were not reset.
T8b949e// Call onHandshakeComplete() β€” this is the production signal that the handshake succeeded
T8b949e// (e.g. NODE_INFO_NONCE arrived). It cancels the armed stall guard and resets the counter.
Te6edf3managerTb4b4b4.Te6edf3onHandshakeCompleteTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Re-arm a fresh Stage 1 stall guard directly. We use startConfigOnly() (the same entry
T8b949e// point handleConnected calls) instead of replaying a Connected emission because the
T8b949e// current state is still Connecting β€” a redundant Connected emission would be rejected by
T8b949e// the redundant-Connected-while-Connecting guard in onConnectionChanged, so handleConnected
T8b949e// would never re-run. The counter is now 0 thanks to the reset above.
Te6edf3managerTb4b4b4.Te6edf3startConfigOnlyTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

T8b949e// Stall #3 fires. Because the counter was reset, priorFailures=0 β†’ NO backoff. The restart
T8b949e// must land immediately, without the 2 s or 4 s delay that an un-reset counter would impose.
Te6edf3advanceTimeByTb4b4b4(T79c0ff1T79c0ff3Te6edf3_000LTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3restartTransportCallsTb4b4b4(Tb4b4b4)Tb4b4b4)
T8b949e// If the counter had NOT been reset, priorFailures would be 2 here and the sibling would be
T8b949e// suspended in delay(4.seconds); restartTransport would still be exactly(2). Reaching
T8b949e// exactly(3) immediately after the stall proves the counter was reset to 0.
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.4.2 - Generated in 0.25s